// 《围棋》作者版权所有。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// Package codehost定义了由代码宿主源代码
// 实现的接口，以及供实现使用的支持代码。
package codehost

import (
	"bytes"
	"crypto/sha256"
	"fmt"
	exec "internal/execabs"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	"cmd/go/internal/cfg"
	"cmd/go/internal/lockedfile"
	"cmd/go/internal/str"
)

// 下载大小限制。
const (
	MaxGoMod   = 16 << 20  // 围棋的最大尺寸。mod file 
	MaxLICENSE = 16 << 20  // 许可证文件的最大大小
	MaxZipFile = 500 << 20 // 下载的zip文件的最大大小
)

// Repo表示代码托管源。典型的实现包括本地版本控制存储库、远程版本控制服务器和代码托管站点。
// 回购协议必须安全，可供多个Goroutine同时使用。
type Repo interface {
	// 列表列出了带有给定前缀的所有标记。
	Tags(prefix string) (tags []string, err error)

	// Stat返回有关修订版本的信息。
	// 修订可以是基础服务已知的任何标识符：
	// 提交哈希、分支、标记等。
	Stat(rev string) (*RevInfo, error)

	// Latest返回默认分支的最新版本，
	// 无论在底层实现中是什么意思。
	Latest() (*RevInfo, error)

	// ReadFile读取与修订版本对应的文件树中的给定文件。
	// 它应该拒绝读取超过maxSize字节的数据。
	// 
	// 如果请求的文件不存在，它应该返回一个错误，因为
	// os。IsNotExist（err）返回true。
	ReadFile(rev, file string, maxSize int64) (data []byte, err error)

	// ReadZip下载给定修订版本的子目录
	// 的zip文件，以在给定临时目录中创建新文件。
	// 它应该拒绝读取超过maxSize字节的数据。
	// 它返回zip文件流式副本的ReadCloser。
	// zip文件中的所有文件都应该是
	// 嵌套在一个顶级目录中，该目录的名称未指定。
	ReadZip(rev, subdir string, maxSize int64) (zip io.ReadCloser, err error)

	// RecentTag返回rev或其前一个版本上的最新标记
	// 。allowed可用于过滤不需要的版本。
	RecentTag(rev, prefix string, allowed func(string) bool) (tag string, err error)

	// DescendsFrom报告rev或其任何祖先是否具有给定的标记。
	// 
	// 对于
	// 相同版本的RecentTag返回的任何标记，DescendsFrom必须返回true。
	DescendsFrom(rev, tag string) (bool, error)
}

// 版本描述源代码存储库中的单个版本。
type RevInfo struct {
	Name    string    // 基础存储库中的完整ID 
	Short   string    // 在伪版本中使用的缩写ID 
	Version string    // 在查找中使用的版本
	Time    time.Time // 提交时间
	Tags    []string  // 提交的已知标记
}

// FileRev描述了在给定版本下读取文件的结果。
type FileRev struct {
	Rev  string // 请求的修订
	Data []byte // 文件数据
	Err  error  // 错误（如果有）；操作系统。IsNotExist（Err）=如果版本存在，但该版本中不存在文件
}

// UnknownRevision错误相当于fs。ErrNotExist，但对于
// 修订版，而不是文件。
type UnknownRevisionError struct {
	Rev string
}

func (e *UnknownRevisionError) Error() string {
	return "unknown revision " + e.Rev
}
func (UnknownRevisionError) Is(err error) bool {
	return err == fs.ErrNotExist
}

// ErrNoCommits是一个相当于fs的错误。ErrNotExist表示给定的
// 存储库或模块不包含提交。
var ErrNoCommits error = noCommitsError{}

type noCommitsError struct{}

func (noCommitsError) Error() string {
	return "no commits"
}
func (noCommitsError) Is(err error) bool {
	return err == fs.ErrNotExist
}

// AllHex报告修订版本是否完全是小写十六进制数字。
func AllHex(rev string) bool {
	for i := 0; i < len(rev); i++ {
		c := rev[i]
		if '0' <= c && c <= '9' || 'a' <= c && c <= 'f' {
			continue
		}
		return false
	}
	return true
}

// ShortenSHA1将SHA1哈希（40个十六进制数字）缩短为标准长度
// 用于伪版本（12个十六进制数字）。
func ShortenSHA1(rev string) string {
	if AllHex(rev) && len(rev) == 40 {
		return rev[:12]
	}
	return rev
}

// WorkDir返回用于给定存储库类型和名称的
// 缓存工作目录的名称。
func WorkDir(typ, name string) (dir, lockfile string, err error) {
	if cfg.GOMODCACHE == "" {
		return "", "", fmt.Errorf("neither GOPATH nor GOMODCACHE are set")
	}

	// 我们为类型和名称的SHA256哈希命名工作目录。
	// 我们故意避免使用实际名称，这既是因为可能的
	// 与有效的文件系统路径冲突，也是因为我们希望确保
	// 一个签出永远不会嵌套在另一个签出中。这种筑巢在过去导致了安全问题。
	if strings.Contains(typ, ":") {
		return "", "", fmt.Errorf("codehost.WorkDir: type cannot contain colon")
	}
	key := typ + ":" + name
	dir = filepath.Join(cfg.GOMODCACHE, "cache/vcs", fmt.Sprintf("%x", sha256.Sum256([]byte(key))))

	if cfg.BuildX {
		fmt.Fprintf(os.Stderr, "mkdir -p %s # %s %s\n", filepath.Dir(dir), typ, name)
	}
	if err := os.MkdirAll(filepath.Dir(dir), 0777); err != nil {
		return "", "", err
	}

	lockfile = dir + ".lock"
	if cfg.BuildX {
		fmt.Fprintf(os.Stderr, "# lock %s", lockfile)
	}

	unlock, err := lockedfile.MutexAt(lockfile).Lock()
	if err != nil {
		return "", "", fmt.Errorf("codehost.WorkDir: can't find or create lock file: %v", err)
	}
	defer unlock()

	data, err := os.ReadFile(dir + ".info")
	info, err2 := os.Stat(dir)
	if err == nil && err2 == nil && info.IsDir() {
		// 信息文件和目录都已存在：重用。
		have := strings.TrimSuffix(string(data), "\n")
		if have != key {
			return "", "", fmt.Errorf("%s exists with wrong content (have %q want %q)", dir+".info", have, key)
		}
		if cfg.BuildX {
			fmt.Fprintf(os.Stderr, "# %s for %s %s\n", dir, typ, name)
		}
		return dir, lockfile, nil
	}

	// 信息文件或目录丢失。从头开始。
	if cfg.BuildX {
		fmt.Fprintf(os.Stderr, "mkdir -p %s # %s %s\n", dir, typ, name)
	}
	os.RemoveAll(dir)
	if err := os.MkdirAll(dir, 0777); err != nil {
		return "", "", err
	}
	if err := os.WriteFile(dir+".info", []byte(key), 0666); err != nil {
		os.RemoveAll(dir)
		return "", "", err
	}
	return dir, lockfile, nil
}

type RunError struct {
	Cmd      string
	Err      error
	Stderr   []byte
	HelpText string
}

func (e *RunError) Error() string {
	text := e.Cmd + ": " + e.Err.Error()
	stderr := bytes.TrimRight(e.Stderr, "\n")
	if len(stderr) > 0 {
		text += ":\n\t" + strings.ReplaceAll(string(stderr), "\n", "\n\t")
	}
	if len(e.HelpText) > 0 {
		text += "\n" + e.HelpText
	}
	return text
}

var dirLock sync.Map

// Run在给定目录
// （空目录表示当前目录）中运行命令行。
// 返回标准输出，对于非零退出，返回一个*RunError，指示命令、退出状态和标准错误。
// 对于成功退出的命令，标准错误不可用。
func Run(dir string, cmdline ...any) ([]byte, error) {
	return RunWithStdin(dir, nil, cmdline...)
}

// bashQuoter转义bash shell中双引号字符串中具有特殊意义的字符。
// 参见https:
var bashQuoter = strings.NewReplacer(`"`, `\"`, `$`, `\$`, "`", "\\`", `\`, `\\`)

func RunWithStdin(dir string, stdin io.Reader, cmdline ...any) ([]byte, error) {
	if dir != "" {
		muIface, ok := dirLock.Load(dir)
		if !ok {
			muIface, _ = dirLock.LoadOrStore(dir, new(sync.Mutex))
		}
		mu := muIface.(*sync.Mutex)
		mu.Lock()
		defer mu.Unlock()
	}

	cmd := str.StringList(cmdline...)
	if os.Getenv("TESTGOVCS") == "panic" {
		panic(fmt.Sprintf("use of vcs: %v", cmd))
	}
	if cfg.BuildX {
		text := new(strings.Builder)
		if dir != "" {
			text.WriteString("cd ")
			text.WriteString(dir)
			text.WriteString("; ")
		}
		for i, arg := range cmd {
			if i > 0 {
				text.WriteByte(' ')
			}
			switch {
			case strings.ContainsAny(arg, "'"):
				// 引用可能会被误认为是引用的args的args。
				text.WriteByte('"')
				text.WriteString(bashQuoter.Replace(arg))
				text.WriteByte('"')
			case strings.ContainsAny(arg, "$`\\*?[\"\t\n\v\f\r \u0085\u00a0"):
				// 引用包含特殊字符、全局模式或空格的参数。
				text.WriteByte('\'')
				text.WriteString(arg)
				text.WriteByte('\'')
			default:
				text.WriteString(arg)
			}
		}
		fmt.Fprintf(os.Stderr, "%s\n", text)
		start := time.Now()
		defer func() {
			fmt.Fprintf(os.Stderr, "%.3fs # %s\n", time.Since(start).Seconds(), text)
		}()
	}
	// TODO:对命令输出大小施加限制。
	// TODO:设置环境以获取英文错误消息。
	var stderr bytes.Buffer
	var stdout bytes.Buffer
	c := exec.Command(cmd[0], cmd[1:]...)
	c.Dir = dir
	c.Stdin = stdin
	c.Stderr = &stderr
	c.Stdout = &stdout
	err := c.Run()
	if err != nil {
		err = &RunError{Cmd: strings.Join(cmd, " ") + " in " + dir, Stderr: stderr.Bytes(), Err: err}
	}
	return stdout.Bytes(), err
}
